// -------------------------------------------------------------------------------------
// Pie Chart view
// Martin D. Flynn, NeXT Computer, Inc.
// -------------------------------------------------------------------------------------

#import <appkit/appkit.h>
#import <libc.h>
#import <c.h>
#import <stdlib.h>
#import <string.h>
#import <math.h>
#import <dpsclient/wraps.h>
#import <objc/List.h>
#import <objc/Storage.h>
#import "PieChartPS.h"
#import "PieChart.h"

// -------------------------------------------------------------------------------------
// archive version number
#define	VERSION			1

// -------------------------------------------------------------------------------------
#define	X				origin.x
#define	Y				origin.y
#define	W				size.width
#define	H				size.height
#define	pieSLICE(I)		((pieSlice_t*)[dataId elementAt:(I)])
#define	matrixCELL(M,I)	(pFlags.useCellTags?[M findCellWithTag:I]:[[M cellList] objectAt:I])
#define	ibSliceCOUNT	((dftSliceCount>0)?dftSliceCount:3)

// -------------------------------------------------------------------------------------
// percent display format
static char	percentFormat[16] = { "%.1f%%" };

// -------------------------------------------------------------------------------------
// pie slice data
typedef struct pieSlice_s {
  id			labelSrc;				// label source for slice
  const char	*label;					// slice label
  id			valueSrc;				// value source for slice
  float			value;					// current slice value
  float			percent;				// current slice percent
  float			endAngle;				// ending angle
} pieSlice_t;

// -------------------------------------------------------------------------------------
@implementation PieChart

// -------------------------------------------------------------------------------------
// archive version number
static BOOL	_nibMode = NO;
+ initialize
{
  [self setVersion:VERSION];
  if (!strcmp([NXApp appName], "InterfaceBuilder")) _nibMode = YES;
  return self;
}

// -------------------------------------------------------------------------------------
// return inspector name
- (const char*)getInspectorClassName { return "PieChartInspector"; }

// -------------------------------------------------------------------------------------
// color/gray support 

/* convert color to gray */
static float _colorGray(NXColor color)
{
  float	gray;
  NXConvertColorToGray(color, &gray);
  return gray;
}

/* set color/gray */
static void _setColor(NXColor color, BOOL isColor)
{
  if (isColor) NXSetColor(color); else PSsetgray(_colorGray(color));
}

// -------------------------------------------------------------------------------------
// color table support

/* return gray value for slice number */
static float _grayValue(int i, int n)
{
  if ((++i) & 1) return (float)(i + 1) / (float)(2 * n);
  return (((float)i / 2.0) + (float)((n + 1) / 2)) / (float)n;
}

- _initColorTable:(int)count clear:(BOOL)clear
{
  int	i;

  /* clear existing table */
  if (clear && sliceColors) { [sliceColors free]; sliceColors = (id)nil; }

  /* set default slice count */
  dftSliceCount = (count >= 0)? count : 0;
  if (dftSliceCount <= 0) return self;
  
  /* build table */
  if (!sliceColors) {
    sliceColors = [[[Storage alloc] initCount:1 elementSize:sizeof(NXColor)
	  description:(char*)nil] empty];
  }
  
  /* fill table */
  for (i = [sliceColors count]; i < dftSliceCount; i++) {
	NXColor color = NXConvertGrayToColor(_grayValue(i, dftSliceCount));
	[sliceColors addElement:&color];
  }
  
  return self;
}

// -------------------------------------------------------------------------------------
// initialize data

/* empty dataId */
- _initPieData
{
  int	i;

  /* create data holder if non-existant */
  if (!dataId) {
    dataId = [Storage newCount:1 elementSize:sizeof(pieSlice_t) description:(char*)nil];
	[dataId empty];
  }
  
  /* free any existing data */
  if (i = [dataId count]) {
	while (i) {
	  const char *lbl = pieSLICE(--i)->label;
	  if (lbl) free((char*)lbl);
	}
	[dataId empty];
  }
  
  return dataId;
}

/* fill dataId */
- _fillPieData:(int)count
{
  int	i;
  for (i = 0; i < count; i++) [self setValue:100.0 forSlice:i];
  return dataId;
}

/* IB use (_nibMode assumed): fill pie with default data */
- _initDefaultPie:(int)sliceCount
{
  [self _initPieData];	// clear existing data
  [self _fillPieData:((sliceCount>=0)?sliceCount:ibSliceCOUNT)];
  return self;
}

// -------------------------------------------------------------------------------------
// initialize view

/* main init */
- initFrame:(const NXRect*)r
{
  
  /* init view */
  [super initFrame:r];

  /* init flags */
  memset(&pFlags, 0, sizeof(pFlags));
  pFlags.showPercent = YES;
  pFlags.drawOutline = YES;
  pFlags.drawLabels  = YES;
  pFlags.drawInColor = YES;
  pFlags.acceptColor = NO;

  /* initialize with defaults */
  outlineColor    = NXConvertGrayToColor(NX_BLACK);
  labelColor	  = NXConvertGrayToColor(NX_BLACK);
  backgroundColor = NXConvertGrayToColor(NX_LTGRAY);
  currentFont	  = [Font newFont:"Helvetica" size:10.0 matrix:NX_IDENTITYMATRIX];
  labelMatrix     = (id)nil;
  valueMatrix     = (id)nil;
  outputPercent	  = (id)nil;
  delegate		  = self;
  
  /* init data holder */
  dataId = (id)nil;
  sliceColors = (id)nil;
  [self setDefaultSliceCount:0];
  [self _initPieData];
  if (_nibMode) [self _fillPieData:ibSliceCOUNT]; 

  return self;
}

/* free pie chart */
- free
{
  [[self _initPieData] free];
  [self setDefaultSliceCount:0];
  return [super free];
}

// -------------------------------------------------------------------------------------
// adding data to the pie chart

/* return cell title */
static const char *_cellTitle(id cellId)
{
  const char *label = (const char*)nil;
  if (!cellId) return (const char*)nil;
  if ([cellId respondsTo:@selector(title)])       label = [cellId title]; else
  if ([cellId respondsTo:@selector(stringValue)]) label = [cellId stringValue];
  return label;
}

/* fill holes in data */
static void _fillData(id dataId, int limit)
{
  int			i;
  pieSlice_t	data = { (id)nil, (const char*)nil, (id)nil, 0.0, 0.0, 0.0 };
  for (i = [dataId count]; i <= limit; i++) [dataId addElement:&data];
}

/* set pie slice data */
- _setPieSlice:(int)slice :(id)vs:(float)v:(BOOL)setV :(id)ls:(char*)l:(BOOL)setL
{
  pieSlice_t	*pieSlice;
  
  /* fill up any holes in data */
  if (slice < 0) return (id)nil;
  _fillData(dataId, slice);
  pieSlice = pieSLICE(slice);
  
  /* set label */
  if (setL) {
	if (pieSlice->label) free((char*)pieSlice->label);
	pieSlice->labelSrc = ls;
	pieSlice->label = (!ls && l)? NXCopyStringBuffer(l) : (const char*)nil;
  }
  
  /* set value */
  if (setV) {
	pieSlice->valueSrc = vs;
	pieSlice->value = (!vs)? v : 0.0;
  }

  return self;
}

/* explicitly set a pie slice value (no re-display) */
- setValue:(float)value forSlice:(int)slice
{
  return [self _setPieSlice:slice :(id)nil:value:YES :(id)nil:(char*)nil:NO];
}

/* set pie slice value and label */
- setValue:(float)value andLabel:(const char*)label forSlice:(int)slice
{
  return [self _setPieSlice:slice :(id)nil:value:YES :(id)nil:(char*)label:YES];
}

/* redisplay */
- update:sender
{
  return [self display];
}

// -------------------------------------------------------------------------------------
// set plot options
// -------------------------------------------------------------------------------------

/* set background transparent */
- setBackgroundTransparent:(BOOL)flag
{
  pFlags.transparent = flag?YES:NO;
  return self;
}

/* return background transparent state */
- (BOOL)backgroundTransparent
{
  return pFlags.transparent;
}

/* set font (make a new unflipped font copy) */
// This is done this way on purpose
- setFont:fontId
{
  currentFont = [Font newFont:[fontId name] size:[fontId pointSize] matrix:NX_IDENTITYMATRIX];  
  return self;
}

/* return current font (NX_FLIPPEDMATRIX) */
// This is done this way on purpose (IB can't handle it properly any other way)
- font
{
  return [Font newFont:[currentFont name] size:[currentFont pointSize]];
}

/* set background gray */
- setBackgroundGray:(float)aGray
{
  backgroundColor = NXConvertGrayToColor(aGray);
  return self;
}

/* return current background gray */
- (float)backgroundGray
{
  float	gray;
  NXConvertColorToGray(backgroundColor, &gray);
  return gray;
}

/* set background color */
- setBackgroundColor:(NXColor)aColor
{
  backgroundColor = aColor;
  return self;
}

/* return current background color */
- (NXColor)backgroundColor
{
  return backgroundColor;
}

/* set outline gray */
- setOutlineGray:(float)aGray
{
  outlineColor = NXConvertGrayToColor(aGray);
  return self;
}

/* return current outline gray */
- (float)outlineGray
{
  float	gray;
  NXConvertColorToGray(outlineColor, &gray);
  return gray;
}

/* set text label gray */
- setLabelGray:(float)aGray
{
  labelColor = NXConvertGrayToColor(aGray);
  return self;
}

/* return current label gray */
- (float)labelGray
{
  float	gray;
  NXConvertColorToGray(labelColor, &gray);
  return gray;
}

/* set show percent on pie slice */
- setShowPercent:(BOOL)flag
{
  pFlags.showPercent = flag?YES:NO;
  return self;
}

/* return show percent status */
- (BOOL)showPercent
{
  return pFlags.showPercent;
}

/* set pie slice outline */
- setDrawSliceOutline:(BOOL)flag
{
  pFlags.drawOutline = flag?YES:NO;
  return self;
}

/* return pie slice outline status */
- (BOOL)drawSliceOutline
{
  return pFlags.drawOutline;
}

/* set draw label */
- setDrawLabels:(BOOL)flag
{
  pFlags.drawLabels = flag?YES:NO;
  return self;
}

/* return draw label status */
- (BOOL)drawLabels
{
  return pFlags.drawLabels;
}

/* set specific slice color */
- setSliceColor:(NXColor)color at:(int)slice
{
  if (slice < 0) return self;
  if (slice + 1 > dftSliceCount) [self _initColorTable:slice + 1 clear:NO];
  [sliceColors replaceElementAt:slice with:&color];
  return self;
}

/* return slice color */
- (NXColor)sliceColorAt:(int)slice
{
  int	count;

  /* check color table */
  if (sliceColors && (slice < [sliceColors count])) {
    NXColor color = *((NXColor*)[sliceColors elementAt:slice]);
	return color;
  }

  /* return gray */
  count = (dftSliceCount > 0)? dftSliceCount : (dataId? [dataId count] : 0);
  if (slice >= count) return NXConvertGrayToColor(NX_WHITE);
  return NXConvertGrayToColor(_grayValue(slice, count));
  
}

/* set default slice count */
- setDefaultSliceCount:(int)count
{
  return [self _initColorTable:count clear:YES];
}

/* return current default slice count */
- (int)defaultSliceCount
{
  return dftSliceCount;
}

/* allow accepting color changes */
- setAcceptColor:(BOOL)flag
{
  pFlags.acceptColor = flag;
  if (flag) pFlags.drawInColor = YES;
  return self;
}

/* set delegate */
- setDelegate:aDelegate
{
  delegate = aDelegate;
  return self;
}

/* return delegate */
- delegate
{
  return delegate;
}

// -------------------------------------------------------------------------------------
// outlets
// -------------------------------------------------------------------------------------

/* load value and label matrix into pie */
- _loadMatrix:sender
{
  int i;
  id vList = valueMatrix? [valueMatrix cellList] : (id)nil;
  id lList = labelMatrix? [labelMatrix cellList] : (id)nil;
  [self _initPieData];	// clear existing data
  if (!vList) return self;
  for (i = 0; i < [vList count]; i++) {
	id vCell = [vList objectAt:i];
	id lCell = (lList && (i < [lList count]))? [lList objectAt:i] : (id)nil;
	[self _setPieSlice:(pFlags.useCellTags?[vCell tag]:i)
					  :vCell:0.0:YES :lCell:(char*)nil:YES];
  }
  [self display];
  return self;
}

/* set value matrix */
- setValueMatrix:anObject
{
  if (![anObject isKindOf:[Matrix class]]) return self;
  valueMatrix = anObject;
  [self perform:@selector(_loadMatrix:) with:(id)nil afterDelay:1 cancelPrevious:YES];
  return self;
}

/* set label matrix */
- setLabelMatrix:anObject
{
  if (![anObject isKindOf:[Matrix class]]) return self;
  labelMatrix = anObject;
  [self perform:@selector(_loadMatrix:) with:(id)nil afterDelay:1 cancelPrevious:YES];
  return self;
}

/* set form matrix */
- setFormMatrix:anObject
{
  if (![anObject isKindOf:[Matrix class]]) return self;
  labelMatrix = valueMatrix = anObject;
  [self perform:@selector(_loadMatrix:) with:(id)nil afterDelay:1 cancelPrevious:YES];
  return self;
}

/* set output percent matrix */
- setPercentMatrix:anObject
{
  if (![anObject isKindOf:[Matrix class]]) return self;
  outputPercent = anObject;
  return self;
}

// -------------------------------------------------------------------------------------
// plot data in pie chart
// -------------------------------------------------------------------------------------

/* pie chart plot */
- _drawPie
{
  int		i, dataCnt = [dataId count];
  float		sum, curAngle, angle;
  
  /* set fonts and gray */
  [currentFont set];
  PSsetlinewidth(1.0);

  /* determine center and radius */
  pieCenter.x = bounds.W / 2.0;
  pieCenter.y = bounds.H / 2.0;
  if (!pFlags.drawLabels) pieRadius = (MIN(bounds.W, bounds.H) - 2.0) / 2.0;
  else pieRadius = (MIN(bounds.W, bounds.H) - (4.5 * [currentFont pointSize])) / 2.0;
  if (pieRadius < 1.0) pieRadius = 1.0;

  /* update values and sum */
  for (sum = 0.0, i = 0; i < dataCnt; i++) {
	pieSlice_t *slice = pieSLICE(i);
	if (slice->valueSrc) slice->value = [slice->valueSrc floatValue];
	sum += slice->value;
  }
  
  /* draw pie ('slice->value' is current) */
  if (sum) {
	char	*lp, label[256], pct[16];
	float	oGray, lGray;
  
	/* draw slices */
	for (curAngle = 0.0, i = 0; i < dataCnt; i++) {
	  pieSlice_t *slice = pieSLICE(i);
      slice->percent = slice->value * 100.0 / sum;
      angle = slice->percent * 3.60;
	  slice->endAngle = curAngle + angle;
      if (!angle) continue;
	  if (lp = (char*)_cellTitle(slice->labelSrc)) strcpy(label, lp); else
      if (slice->label) strcpy(label, slice->label);
	  else sprintf(label, "#%d", i + 1);
	  if (pFlags.showPercent) {
		sprintf(pct, percentFormat, slice->percent);
	    sprintf(&label[strlen(label)], " (%s)", pct);
	  }
	  _setColor([self sliceColorAt:i], pFlags.drawInColor);
	  NXConvertColorToGray(outlineColor, &oGray);
	  NXConvertColorToGray(labelColor, &lGray);
      _pieDrawSlice(label, [currentFont pointSize], pieCenter.x, pieCenter.y, pieRadius,
	  		curAngle, slice->endAngle, (pFlags.drawOutline?oGray:-1.0),
			(pFlags.drawLabels?lGray:-1.0));
	  curAngle += angle;
    }
  
	/* update output percent matrix */
	if (outputPercent) {
	  for (i = 0; i < dataCnt; i++) {
		id cellId = matrixCELL(outputPercent, i);
		if (cellId) {
		  pieSlice_t *slice = pieSLICE(i);
		  sprintf(pct, percentFormat, slice->percent);
		  [cellId setStringValue:pct];
		}
	  }
	}
	
  }

  return self;
  
}
  
/* draw pie view */
- drawSelf:(const NXRect *)r :(int)n
{
  if (!pFlags.transparent) {
	_setColor(backgroundColor, pFlags.drawInColor);
	NXRectFill(&bounds);
  }
  [self _drawPie];
//if (isFirstResponder) NXHighlightRect(&bounds);
  return self;
}
  
// -------------------------------------------------------------------------------------
// object archiving

- read:(NXTypedStream*)s
{
  int	ver, i, cnt;
  sliceColors = (id)nil;
  [super read:s];
  NXReadTypes(s, "i",&ver);
  NXReadTypes(s, "s@",&pFlags,&currentFont);
  backgroundColor = NXReadColor(s);
  outlineColor    = NXReadColor(s);
  labelColor      = NXReadColor(s);
  NXReadTypes(s, "i",&cnt);
  [self setDefaultSliceCount:cnt];	// set dftSliceCount
  for (i = 0; i < dftSliceCount; i++) [self setSliceColor:NXReadColor(s) at:i];
  delegate		 = self;
  labelMatrix    = (id)nil;
  valueMatrix    = (id)nil;
  outputPercent	 = (id)nil;
  dataId		 = (id)nil;
  pFlags.drawInColor = YES;
  [self _initPieData];
  if (_nibMode) [self _fillPieData:ibSliceCOUNT];
  return self;
}

- write:(NXTypedStream*)s
{
  int	ver = [[self class] version];
  int	i;
  [super write:s];
  NXWriteTypes(s, "i",&ver);
  NXWriteTypes(s, "s@",&pFlags,&currentFont);
  NXWriteColor(s, backgroundColor);
  NXWriteColor(s, outlineColor);
  NXWriteColor(s, labelColor);
  NXWriteTypes(s, "i",&dftSliceCount);
  for (i = 0; i < dftSliceCount; i++) NXWriteColor(s, [self sliceColorAt:i]);
  return self;
}

// -------------------------------------------------------------------------------------
// firstResponder for getting copy/paste commands

/* allow becoming first responder */
- (BOOL)acceptsFirstResponder { return YES; }

/* become first responder */
- becomeFirstResponder
{
  isFirstResponder = YES;
  [self display];
  return self;
}

/* resign first responder */
- resignFirstResponder
{
  isFirstResponder = NO;
  [self display];
  return self;
}

// -------------------------------------------------------------------------------------
// mouseDown tracking

/* handle pie slice selected */
- selectedPieSlice:(int)slice
{
  id	cellId, matrix = actionMatrix;
  if (!matrix && valueMatrix && ([valueMatrix target] != self)) matrix = valueMatrix;
  if (matrix && (slice >= 0) && (cellId = matrixCELL(matrix, slice))) {
	[matrix selectCell:cellId];
	[matrix sendAction];
  }
  return self;
}

/* slice hit detection */
- (int)_sliceHitDetection:(const NXPoint*)mousePoint
{
  int		index, dataCnt = dataId? [dataId count] : 0;
  float		len, xLen, yLen, angle;
  NXPoint	pt = *mousePoint;
  
  /* check for valid pie */
  if (!dataCnt) return -1;
  [self convertPoint:&pt fromView:(id)nil];
  
  /* determine distance from center */
  xLen = pt.x - pieCenter.x;
  yLen = pt.y - pieCenter.y;
  len = (float)hypot((double)xLen, (double)yLen);
  if (len > pieRadius) return -1;
  
  /* determine angle */
  angle = (float)(atan2(yLen, xLen) * 180.0 / M_PI);
  if (angle < 0.0) angle += 360.0;
  
  /* find pie slice */
  for (index = 0; index < dataCnt; index++) {
	pieSlice_t *slice = pieSLICE(index);
	if (angle <= slice->endAngle) return index;
  }
  
  return -1;
}
 
/* intercept mouse down */
- mouseDown:(NXEvent*)e
{
  int	index;

  /* IB check */
  if (_nibMode && (delegate != self))
	return [delegate selectedPieSlice:[self _sliceHitDetection:&(e->location)]]; 
  
  /* check for slice selection */
  if (e->data.mouse.click == 2) {
	index = [self _sliceHitDetection:&(e->location)];
	if ((index >= 0) && [delegate respondsTo:@selector(selectedPieSlice:)])
	  [delegate selectedPieSlice:index];
  }
  
  return [super mouseDown:e];
}

// -------------------------------------------------------------------------------------
// IB use: color modification

/* accept color */
- acceptColor:(NXColor)color atPoint:(const NXPoint*)point
{
  int	slice;
  if (!pFlags.acceptColor) return self;
  slice = [self _sliceHitDetection:point];
  if (slice < 0) [self setBackgroundColor:color];
  else if (dftSliceCount > 0) [self setSliceColor:color at:slice];
  return [self display];
}

@end
